﻿using System;
using System.Collections.Generic;
using System.Text;
#if(!WindowsCE)
using System.Linq.Expressions;
#endif
using ValidationFramework.Extensions;
using ValidationFramework.Reflection;
using ValidationFramework.ErrorMessage;
using System.Linq;
using System.Reflection;

namespace ValidationFramework
{
	/// <summary>
	/// Manages property validation state for a given object and rule set. This will be used internally by the given object to support self validation and the reporting of its state to consumers.
	/// </summary>
	public class ValidationManager
	{
		// --- Fields

		/// <summary>
		/// Holds PropertyValueRules for property keys
		/// </summary>
		private PropertyRuleDictionary<PropertyValueRule> propertyValueRuleDictionary;
		private PropertyRuleDictionary<PropertyStateRule> propertyStateRuleDictionary;
		private ValidationErrorDictionary errorDictionary;
		private IPropertyRuleRepository ruleRepository;
		private ErrorMessageResolver messageResolver;

		// --- Properties

		/// <summary>
		/// Gets a value indicating if all members are valid.
		/// </summary>
		/// <remarks>Calling this property does not perform a validation it only checks the current state. To perform a full validation call 
		/// <see cref="MemberValidationManager{T}.ValidateAll()"/>.</remarks>
		public bool IsValid
		{
			get
			{
				return errorDictionary.GetAllErrors().Count == 0;
			}
		}

		/// <summary>
		/// Gets the instance of the object that this <see cref="MemberValidationManager{T}"/> is handling.
		/// </summary>
		/// <remarks>Will return a null for static types.</remarks>
		public object ManagedObject
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets the <see cref="RuntimeTypeHandle"/> for the <see cref="Type"/> being validated.
		/// </summary>
		public RuntimeTypeHandle ManagedObjectTypeHandle
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets the rule set to this is managing.
		/// </summary>
		/// <remarks>
		/// Only <see cref="PropertyRule"/>s where RuleSet equals this value.
		/// Case insensitive so this will always return a upper case string no matter what is passed into the constructor.
		/// Returns an empty string if All rule sets are being managed.
		/// </remarks>
		public string RuleSet { get; private set; }

		/// <summary>
		/// Gets the number of rules this is managing.
		/// </summary>
		public int RulesCount
		{
			get
			{
				var count = 0;
				foreach(var ruleList in propertyValueRuleDictionary)
                {
					count += ruleList.Value.Count;
                }
				foreach(var ruleList in propertyStateRuleDictionary)
                {
					count += ruleList.Value.Count;
                }
				return count;
			}
		}

		/// <summary>
		/// An array of property names this is managing.
		/// </summary>
		public string[] ManagedPropertyNames { get; private set; }

		// --- Constructors

		/// <summary>
		/// Initialize instance of validation manager
		/// </summary>
		/// <param name="instanceToManage"></param>
		public ValidationManager(object instanceToManage)
		{
			Guard.ArgumentNotNull(instanceToManage, "targetObject");
			Guard.ArgumentNotNull(ruleRepository, "ruleRepository");
			RuntimeTypeHandle managedTypeHandle;
			this.ManagedObject = instanceToManage;
#if(WindowsCE || SILVERLIGHT)
			managedTypeHandle = instanceToManage.GetType().TypeHandle;
#else
			managedTypeHandle = Type.GetTypeHandle(instanceToManage);
#endif
			this.ManagedObjectTypeHandle = managedTypeHandle;
			this.propertyValueRuleDictionary = new PropertyRuleDictionary<PropertyValueRule>(managedTypeHandle);
			this.propertyStateRuleDictionary = new PropertyRuleDictionary<PropertyStateRule>(managedTypeHandle);
			this.errorDictionary = new ValidationErrorDictionary(managedTypeHandle);

			this.RuleSet = string.Empty;
			this.messageResolver = new ErrorMessageResolver();
			this.ruleRepository = TypeCache.GetType(managedTypeHandle);
			this.ManagedPropertyNames = ruleRepository.GetPropertyNames();

			this.LoadRules();
		}


		// --- Methods

		public void ChangeRuleSet(string ruleSet)
		{
			if (!string.IsNullOrEmpty(ruleSet))
			{
				this.RuleSet = ruleSet;
			}
			else
			{
				this.RuleSet = string.Empty;
			}
			this.LoadRules();
		}

		// --- Methods | Get Errors
		/// <summary>
		/// Get a <see cref="IList{T}"/> of all this manager's <see cref="ValidationError"/>s.
		/// </summary>
		public IList<ValidationError> GetErrors()
		{
			return errorDictionary.GetAllErrors();
		}

		/// <summary>
		/// Get a <see cref="IList{T}"/> of all <see cref="ValidationError"/>s for a given member.
		/// </summary>
		/// <remarks>Use <see cref="ResultFormatter"/> for some basic formatting conversions of <see cref="ValidationResult"/>s.</remarks>
		/// <param name="propertyName">Member to retrieve error message for. Case sensitive.</param>
		/// <returns>All <see cref="ValidationError"/>s for a given member.</returns>
		/// <exception cref="ArgumentNullException"><paramref name="propertyName"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="propertyName"/> is <see cref="string.Empty"/>.</exception>
		/// <exception cref="ArgumentException">No <see cref="InfoDescriptor"/> could be found named <paramref name="propertyName"/>.</exception>
		public IList<ValidationError> GetErrors(string propertyName)
		{
			Guard.ArgumentNotNullOrEmptyString(propertyName, "propertyName");
			if (!errorDictionary.ContainsKey(propertyName))
			{
				throw new ArgumentException(string.Format("A property named '{0}' could not be found in error dictionary.", propertyName), "propertyName");
			}
			return errorDictionary[propertyName];
		}

		/// <summary>
		/// Get a <see cref="IList{T}"/> of all <see cref="ValidationError"/>s for a list of properties.
		/// </summary>
		/// <remarks>Use <see cref="ResultFormatter"/> for some basic formatting conversions of <see cref="ValidationResult"/>s.</remarks>
		/// <param name="propertyNames">The names of the properties to retrieve error messages for. Case sensitive.</param>
		/// <returns>All <see cref="ValidationError"/>s for the list of properties.</returns>
		/// <exception cref="ArgumentNullException"><paramref name="propertyNames"/> is null.</exception>
		/// <exception cref="ArgumentOutOfRangeException">Length of <paramref name="propertyNames"/> is 0.</exception>
		/// <exception cref="ArgumentException">No <see cref="InfoDescriptor"/> could be found any single member <paramref name="propertyNames"/>.</exception>
		public IList<ValidationError> GetErrors(params string[] propertyNames)
		{
			Guard.ArgumentNotNull(propertyNames, "propertyNames");
			if (propertyNames.Length == 0)
			{
				throw new ArgumentOutOfRangeException("propertyNames");
			}

			var errors = new List<ValidationError>();
			foreach (var memberName in propertyNames)
			{
				errors.AddRange(GetErrors(memberName));
			}
			return errors;
		}
#if (!WindowsCE)
		/// <summary>
		/// Get a <see cref="IList{T}"/> of all <see cref="ValidationError"/>s for a given member.
		/// </summary>
		/// <remarks>Use <see cref="ResultFormatter"/> for some basic formatting conversions of <see cref="ValidationResult"/>s.</remarks>
		/// <param name="expression">The <see cref="Expression{TDelegate}"/> to retrieve error message for. </param>
		/// <returns>All <see cref="ValidationError"/>s for a given member.</returns>
		/// <exception cref="ArgumentNullException"><paramref name="expression"/> is null.</exception>
		/// <exception cref="ArgumentException">No <see cref="InfoDescriptor"/> could be found based on <paramref name="expression"/>.</exception>
		public IList<ValidationError> GetErrors<T>(Expression<Func<T>> expression)
		{
			return GetErrors(TypeExtensions.GetMemberName(expression));
		}
#endif
		/// <summary>
		/// Get a <see cref="IList{T}"/> of all <see cref="ValidationError"/>s for a given member. No exception is thrown if the member is not found.
		/// </summary>
		/// <param name="propertyName">Member to retrieve error message for. Case sensitive.</param>
		/// <param name="ValidationError">When this method returns, if the member exists and contains <see cref="Rule"/>s, contains a <see cref="IList{T}"/> of <see cref="ValidationResult"/> for the member being validated; otherwise null.</param>
		/// <return><see langword="true"/> if the member exists and has <see cref="Rule"/>s; otherwise <see langword="false"/>.</return>
		/// <exception cref="ArgumentNullException"><paramref name="propertyName"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="propertyName"/> is <see cref="string.Empty"/>.</exception>
		public bool TryGetErrors(string propertyName, out IList<ValidationError> errors)
		{
			Guard.ArgumentNotNullOrEmptyString(propertyName, "propertyName");
			if (!this.errorDictionary.ContainsKey(propertyName))
			{
				errors = null;
				return false;
			}

			errors = errorDictionary[propertyName];
			return true;
		}
#if (!WindowsCE)
		/// <summary>
		/// Get a <see cref="IList{T}"/> of all <see cref="ValidationResult"/>s for a given member. No exception is thrown if the member is not found.
		/// </summary>
		/// <remarks>Use <see cref="ResultFormatter"/> for some basic formatting conversions of <see cref="ValidationResult"/>s.</remarks>
		/// <param name="expression">The <see cref="Expression{TDelegate}"/> to retrieve error message for. Case sensitive.</param>
		/// <param name="validationResults">When this method returns, if the member exists and contains <see cref="Rule"/>s, contains a <see cref="IList{T}"/> of <see cref="ValidationResult"/> for the member being validated; otherwise null.</param>
		/// <return><see langword="true"/> if the member exists and has <see cref="Rule"/>s; otherwise <see langword="false"/>.</return>
		/// <exception cref="ArgumentNullException"><paramref name="expression"/> is null.</exception>
		public bool TryGetErrors<T>(Expression<Func<T>> expression, out IList<ValidationError> errors)
		{
			return TryGetErrors(TypeExtensions.GetMemberName(expression), out errors);
		}
#endif

		// --- Methods | Validation

		/// <summary>
		/// This will validate all managed property rules and update this manager's state to reflect the results.
		/// </summary>
		public void Validate()
		{
			this.errorDictionary.Clear();
			foreach (var propertyName in this.ManagedPropertyNames)
			{
				Validate(propertyName);
			}
		}

		/// <summary>
		/// This will validate all managed property rules and update this manager's state to reflect the results.
		/// </summary>
		/// <remarks>
		/// This will often be called to re-validate a managed type after one of its properties has changed.
		/// </remarks>
		public void Validate(string propertyName)
		{
			this.errorDictionary.ResetKey(propertyName);

			var valueRuleErrors = TestPropertyValueRules(this.propertyValueRuleDictionary[propertyName]);
			var stateRuleErrors = TestPropertyStateRules(this.propertyStateRuleDictionary[propertyName]);

			var errors = new List<ValidationError>();
			errors.AddRange(valueRuleErrors);
			errors.AddRange(stateRuleErrors);

			this.errorDictionary[propertyName] = errors;

		}

		// --- Methods | Validation Internal

		/// <summary>
		/// Invokes specified property get method on managed object with the fast method invoker. This by passes traditional reflection for speed.
		/// </summary>
		private object FastInvokePropertyGet(string propertyName)
		{
			var propertyGetMethodInfo = this.ManagedObject.GetType().GetProperty(propertyName).GetGetMethod();
			var fastInvoker = MethodInvokerCreator.GetMethodInvoker(propertyGetMethodInfo);
			return fastInvoker.Invoke(this.ManagedObject, null);
		}
		private List<ValidationError> TestPropertyValueRules(IList<PropertyValueRule> rules)
		{
			var errors = new List<ValidationError>();

			foreach (var valueRule in rules)
			{
				var validator = valueRule.Validator;
				// For speed, use fast method invoker instead of reflection
				var currentPropertyValue = this.FastInvokePropertyGet(valueRule.PropertyName);

				if (!validator.Validate(currentPropertyValue))
				{
					var attemptedValue = currentPropertyValue;
					var propertyName = valueRule.PropertyName;
					// TODO: The implicit relationship between the rule and the managed type is messy. Consider making this apart of the rule.
					var typeName = this.ManagedObject.GetType().Name;
					var messageContext = new PropertyMessageContext(propertyName, typeName);
					var errorMessage = messageResolver.ResolvePropertyErrorMessage(valueRule, currentPropertyValue, messageContext);
					
					errors.Add(new ValidationError(errorMessage, attemptedValue, propertyName, typeName));
				}
			}
			return errors;
		}
		private List<ValidationError> TestPropertyStateRules(IList<PropertyStateRule> rules)
		{
			var errors = new List<ValidationError>();

			foreach (var stateRule in rules)
			{
				var validator = stateRule.Validator;
				// For speed, use fast method invoker instead of reflection
				var currentPropertyValue = this.FastInvokePropertyGet(stateRule.PropertyName);

				if (!validator.Validate(currentPropertyValue, this.ManagedObject))
				{
					var attemptedValue = currentPropertyValue;
					var propertyName = stateRule.PropertyName;
					// TODO: The implicit relationship between the rule and the managed type is messy. Consider making this apart of the rule.
					var typeName = this.ManagedObject.GetType().Name;
					var messageContext = new PropertyMessageContext(propertyName, typeName);
					var errorMessage = messageResolver.ResolvePropertyErrorMessage(stateRule, currentPropertyValue, messageContext);

					errors.Add(new ValidationError(errorMessage, attemptedValue, propertyName, typeName));
				}
			}

			return errors;
		}

		// --- Methods | Load Rules

		/// <summary>
		/// Fills propertyValueRules and propertyStateRules for this manager's rule set from the repository
		/// </summary>
		private void LoadRules()
		{
			this.LoadValueRules();
			this.LoadStateRules();
		}		
		private void LoadValueRules()
		{
			// grab value rules from repository
			var valueRules = ruleRepository.GetPropertyValueRules();
			// filter the rules if this is managing a rule set
			if (!string.IsNullOrEmpty(this.RuleSet))
			{
				var q = valueRules.Where(x => x.RuleSet.ToUpperInvariant() == this.RuleSet.ToUpperInvariant());
				valueRules = q.ToList();
			}

			this.propertyValueRuleDictionary.Reset();
			this.propertyValueRuleDictionary.AddRules(valueRules);
		}
		private void LoadStateRules()
		{
			// grab state rules from repository
			var stateRules = ruleRepository.GetPropertyStateRules();
			// filter the rules if this is managing a rule set
			if (!string.IsNullOrEmpty(this.RuleSet))
			{
				var q = stateRules.Where(x => x.RuleSet.ToUpperInvariant() == this.RuleSet.ToUpperInvariant());
				stateRules = q.ToList();
			}

			this.propertyStateRuleDictionary.Reset();
			this.propertyStateRuleDictionary.AddRules(stateRules);
		}
	}
}
